package androidx.viewpager2.widget;

import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import androidx.core.view.NestedScrollingParent3;
import androidx.core.view.NestedScrollingParentHelper;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.PagerSnapHelper;
import androidx.recyclerview.widget.RecyclerView;

public final class ViewPager3 extends RecyclerView implements NestedScrollingParent3
{
    public static boolean DEBUG = true;
    private LinearLayoutManager mLayoutManager;
    private int mCurrentItem = 0;
    private int mPendingCurrentItem = -1;
    private boolean mPendingSmooth = false;
    private List<OnPageChangeCallback> mPageChangeCallbacks;
    private NestedScrollingParentHelper mParentHelper;
    private int mMotionX, mMotionY;
    private int mChildScrollState;
    private Rect mChildRect;
    private SparseArray<Boolean> mNestedScrollStarted = new SparseArray<>();
    private int[] mNestedScroll = {0, 0};
    private float mDragSlop = 150;

    public ViewPager3(Context context){
        super(context);
        init(context, null);
    }

    public ViewPager3(Context context, AttributeSet attrs){
        super(context, attrs);
        init(context, attrs);
    }

    public ViewPager3(Context context, AttributeSet attrs, int defStyleAttr){
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs){
        mParentHelper = new NestedScrollingParentHelper(this);
        if(mDragSlop > 1) mDragSlop *= getResources().getDisplayMetrics().density;
        new PagerSnapHelper().attachToRecyclerView(this);
        if(getLayoutManager() instanceof LinearLayoutManager){
            mLayoutManager = (LinearLayoutManager)getLayoutManager();
        }else{
            mLayoutManager = new LinearLayoutManager(context);
            setLayoutManager(mLayoutManager);
        }
        addOnScrollListener(new LocalScrollListener());
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev){
        if(DEBUG) Log.i("----", "VVV onInterceptTouchEvent " + Arrays.asList(//
            MotionEvent.actionToString(ev.getAction()),//
            (int)ev.getY(),//
            "mChildScrollState",//
            mChildScrollState,//
            ""//
        ));
        int oldX = mMotionX;
        int oldY = mMotionY;
        mMotionX = (int)ev.getX();
        mMotionY = (int)ev.getY();
        if(ev.getAction() == MotionEvent.ACTION_DOWN){
            mChildScrollState = 0;
        }else{
            if(mChildScrollState == 1) return false;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int axes){
        if(DEBUG) Log.i("----", "onStartNestedScroll*");
        return onStartNestedScroll(child, target, axes, ViewCompat.TYPE_TOUCH);
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int axes, int type){
        boolean ok = false;
        if(getOrientation() != VERTICAL){
            ok = (axes & ViewCompat.SCROLL_AXIS_HORIZONTAL) != 0;
        }else{
            ok = (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
        }
        if(DEBUG) Log.i("----", "onStartNestedScroll " + Arrays.asList(//
            "type",//
            type,//
            "axes",//
            axes,//
            "ok",//
            ok,//
            ""//
        ));
        return ok;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int axes){
        if(DEBUG) Log.i("----", "onNestedScrollAccepted*");
        onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH);
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int axes, int type){
        if(DEBUG) Log.i("----", "onNestedScrollAccepted " + Arrays.asList(//
            "type",//
            type,//
            "axes",//
            axes,//
            ""//
        ));
        mParentHelper.onNestedScrollAccepted(child, target, axes, type);
        startNestedScroll(axes, type);
        if(mChildScrollState == 0) mChildScrollState = 1;
    }

    @Override
    public void onStopNestedScroll(View target){
        if(DEBUG) Log.i("----", "onStopNestedScroll*");
        onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
    }

    @Override
    public void onStopNestedScroll(View target, int type){
        if(DEBUG) Log.i("----", "onStopNestedScroll " + Arrays.asList(//
            "type",//
            type,//
            "mNestedScrollStarted",//
            mNestedScrollStarted,//
            ""//
        ));
        mParentHelper.onStopNestedScroll(target, type);
        stopNestedScroll(type);
        boolean started = Objects.equals(true, mNestedScrollStarted.get(type));
        mNestedScrollStarted.put(type, false);
        if(type != ViewCompat.TYPE_TOUCH){
            //todo Fling
            return;
        }
        if(started){
            View child = findChild(target);
            int position = ((LayoutParams)child.getLayoutParams()).getViewAdapterPosition();
            LinearLayoutManager lm = mLayoutManager;
            int offset = 0;
            int scroll = 0;
            if(getOrientation() != VERTICAL){
                offset = lm.getLeftDecorationWidth(child);
                offset -= ((LayoutParams)child.getLayoutParams()).leftMargin;
                scroll = mNestedScroll[0];
            }else{
                offset = lm.getDecoratedTop(child);
                offset -= ((LayoutParams)child.getLayoutParams()).topMargin;
                scroll = mNestedScroll[1];
            }
            int offset2 = Math.abs(offset);
            float total = computeVerticalScrollExtent();
            if(offset2 >= (mDragSlop > 1? mDragSlop: mDragSlop * total)){
                int offset3 = Math.round(Math.max(1, offset2 / total));
                if(offset > 0){
                    //页面被拉下来
                    if(scroll > 0) offset3 -= 1;//又被拉回
                    position -= offset3;
                }else{
                    //页面被拉上来
                    if(scroll < 0) offset3 -= 1;//又被拉回
                    position += offset3;
                }
            }
            smoothScrollToPosition(position);
        }
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed){
        if(DEBUG) Log.i("----", "onNestedPreScroll*");
        onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type){
        if(DEBUG) Log.i("----", "onNestedPreScroll " + Arrays.asList(//
            "mChildScrollState",//
            mChildScrollState,//
            "dx",//
            dx,//
            "dy",//
            dy,//
            ""//
        ));
        mNestedScrollStarted.put(type, true);
        dispatchNestedPreScroll(dx, dy, consumed, null, type);
        if(consumed != null){
            dx -= consumed[0];
            dy -= consumed[1];
        }
        if(dx != 0 || dy != 0){
            int[] scroll = computeNestedScroll(target, dx, dy);
            scrollBy(scroll[0], scroll[1]);
            mNestedScroll[0] = scroll[0];
            mNestedScroll[1] = scroll[1];
        }
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
        int dyUnconsumed){
        if(DEBUG) Log.i("----", "onNestedScroll*");
        onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
            ViewCompat.TYPE_TOUCH, null);
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
        int dyUnconsumed, int type){
        if(DEBUG) Log.i("----", "onNestedScroll*");
        onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, null);
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
        int dyUnconsumed, int type, int[] consumed){
        if(DEBUG) Log.i("----", "onNestedScroll " + Arrays.asList(//
            "dxConsumed",//
            dxConsumed,//
            "dyConsumed",//
            dyConsumed,//
            "dxUnconsumed",//
            dxUnconsumed,//
            "dyUnconsumed",//
            dyUnconsumed,//
            ""//
        ));
        if(type != ViewCompat.TYPE_TOUCH){
            //todo Fling
            return;
        }
        boolean isVertical = getOrientation() == VERTICAL;
        if(mChildScrollState == 1){
            if(!isVertical? dxConsumed == 0: dyConsumed == 0) mChildScrollState = -1;
        }
        if(!isVertical){
            if(dxUnconsumed == 0) return;
            final int scrollOffset = computeHorizontalScrollOffset();
            scrollBy(dxUnconsumed, 0);
            final int myConsumed = computeHorizontalScrollOffset() - scrollOffset;
            if(consumed != null) consumed[0] += myConsumed;
            final int myUnconsumed = dxUnconsumed - myConsumed;
            dispatchNestedScroll2(myConsumed, 0, myUnconsumed, 0, null, type, consumed);
        }else{
            if(dyUnconsumed == 0) return;
            final int scrollOffset = computeVerticalScrollOffset();
            scrollBy(0, dyUnconsumed);
            final int myConsumed = computeVerticalScrollOffset() - scrollOffset;
            if(consumed != null) consumed[1] += myConsumed;
            final int myUnconsumed = dyUnconsumed - myConsumed;
            dispatchNestedScroll2(0, myConsumed, 0, myUnconsumed, null, type, consumed);
        }
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY){
        if(DEBUG) Log.i("----", "onNestedPreFling " + Arrays.asList(//
            "velocityX",//
            (int)velocityX,//
            "velocityY",//
            (int)velocityY,//
            ""//
        ));
        return dispatchNestedPreFling(velocityX, velocityY);
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed){
        if(DEBUG) Log.i("----", "onNestedFling " + Arrays.asList(//
            "velocityX",//
            (int)velocityX,//
            "velocityY",//
            (int)velocityY,//
            "consumed",//
            consumed,//
            ""//
        ));
        boolean done = dispatchNestedFling(velocityX, velocityY, consumed);
        if(true) return done;
        fling((int)velocityX, (int)velocityY);
        return true;
    }

    @Override
    public int getNestedScrollAxes(){
        return mParentHelper.getNestedScrollAxes();
    }

    public void dispatchNestedScroll2(int dxConsumed, int dyConsumed, int dxUnconsumed,
        int dyUnconsumed, int[] offsetInWindow, int type, int[] consumed){
        if(DEBUG) Log.i("----", "dispatchNestedScroll " + Arrays.asList(//
            "dxConsumed",//
            dxConsumed,//
            "dyConsumed",//
            dyConsumed,//
            "dxUnconsumed",//
            dxUnconsumed,//
            "dyUnconsumed",//
            dyUnconsumed,//
            ""//
        ));
        mNestedScroll[0] = dxConsumed;
        mNestedScroll[1] = dyConsumed;
        super.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
            offsetInWindow, type, consumed);
    }

    int[] computeNestedScroll(View target, int... xy){
        View child = findChild(target);
        if(child == null){
            xy[0] = xy[1] = 0;
            return xy;
        }

        LinearLayoutManager lm = mLayoutManager;
        Rect rect = mChildRect;
        if(mChildRect == null) mChildRect = rect = new Rect();
        rect.left = lm.getDecoratedLeft(child);
        rect.top = lm.getDecoratedTop(child);
        rect.right = lm.getDecoratedRight(child);
        rect.bottom = lm.getDecoratedBottom(child);
        LayoutParams lp = (LayoutParams)child.getLayoutParams();
        rect.left -= lp.leftMargin;
        rect.top -= lp.topMargin;
        rect.right += lp.rightMargin;
        rect.bottom += lp.bottomMargin;
        if(DEBUG) Log.i("----", "computeNestedScroll " + rect);

        if(getOrientation() != VERTICAL){
            if(xy[0] > 0){
                xy[0] = Math.min(xy[0], Math.max(0, rect.left));
            }else{
                xy[0] = Math.max(xy[0], Math.min(0, rect.left));
            }
        }else{
            if(xy[1] > 0){
                xy[1] = Math.min(xy[1], Math.max(0, rect.top));
            }else{
                xy[1] = Math.max(xy[1], Math.min(0, rect.top));
            }
        }
        return xy;
    }

    View findChild(View child){
        while(child != null){
            ViewParent parent = child.getParent();
            if(parent == this) break;
            child = parent instanceof View? (View)parent: null;
        }
        return child;
    }

    @Orientation
    public int getOrientation(){
        return mLayoutManager.getOrientation();
    }

    public void setOrientation(@Orientation int orientation){
        mLayoutManager.setOrientation(orientation);
    }

    public int getCurrentItem(){
        return mCurrentItem;
    }

    public void setCurrentItem(int item){
        setCurrentItem(item, true);
    }

    public void setCurrentItem(int item, boolean smooth){
        if(getAdapter() == null){
            mPendingCurrentItem = item;
            mPendingSmooth = smooth;
            return;
        }
        _setCurrentItem(item, smooth);
    }

    private void _setCurrentItem(int item, boolean smooth){
        Adapter adapter = getAdapter();
        if(adapter == null) return;
        mPendingCurrentItem = -1;
        item = Math.min(item, adapter.getItemCount());
        if(mCurrentItem == item) return;
        mCurrentItem = item;
        if(smooth) smoothScrollToPosition(item);
        else scrollToPosition(item);
    }

    @Override
    public void setAdapter(Adapter adapter){
        super.setAdapter(adapter);
        if(mPendingCurrentItem >= 0){
            _setCurrentItem(mPendingCurrentItem, mPendingSmooth);
        }
    }

    public void registerOnPageChangeCallback(OnPageChangeCallback callback){
        if(callback == null) return;
        if(mPageChangeCallbacks == null) mPageChangeCallbacks = new ArrayList<>();
        if(mPageChangeCallbacks.contains(callback)) return;
        mPageChangeCallbacks.add(callback);
    }

    public void unregisterOnPageChangeCallback(OnPageChangeCallback callback){
        if(callback == null) return;
        mPageChangeCallbacks.remove(callback);
    }

    private void dispatchScrollStateChanged(int state){
        if(mPageChangeCallbacks == null) return;
        for(OnPageChangeCallback e: mPageChangeCallbacks) e.onPageScrollStateChanged(state);
    }

    private void dispatchScrolled(int p, float ratio, int offset){
        if(mPageChangeCallbacks == null) return;
        for(OnPageChangeCallback e: mPageChangeCallbacks) e.onPageScrolled(p, ratio, offset);
        if(mPageChangeCallbacks == null) return;
    }

    private void dispatchSelected(int p){
        mCurrentItem = p;
        if(mPageChangeCallbacks == null) return;
        for(OnPageChangeCallback e: mPageChangeCallbacks) e.onPageSelected(p);
    }

    class LocalScrollListener extends OnScrollListener
    {
        int mPosition = -1;
        int mScrollState = SCROLL_STATE_SETTLING;

        @Override
        public void onScrollStateChanged(RecyclerView pa, int state){
            mScrollState = state;
            dispatchScrollStateChanged(state);
        }

        @Override
        public void onScrolled(RecyclerView pa, int dx, int dy){
            LinearLayoutManager lm = mLayoutManager;
            int position = lm.findFirstVisibleItemPosition();
            View child = lm.findViewByPosition(position);
            int total;
            int offset;
            if(getOrientation() != VERTICAL){
                int decoration = lm.getLeftDecorationWidth(child);
                total = child.getWidth() + decoration + lm.getRightDecorationWidth(child);
                offset = child.getLeft() - decoration - pa.getPaddingLeft();
            }else{
                int decoration = lm.getTopDecorationHeight(child);
                total = child.getHeight() + decoration + lm.getBottomDecorationHeight(child);
                offset = child.getTop() - decoration - pa.getPaddingTop();
            }
            offset = -offset;
            float ratio = total == 0? 0: offset / (float)total;
            dispatchScrolled(position, ratio, offset);
            if(mScrollState == SCROLL_STATE_SETTLING){
                if(mPosition != position){
                    mPosition = position;
                    dispatchSelected(position);
                }
            }
        }
    }

    public abstract static class OnPageChangeCallback
    {
        public void onPageScrollStateChanged(int state){
        }

        public void onPageScrolled(int p, float ratio, int offset){
        }

        public void onPageSelected(int p){
        }
    }
}
